/**
 * Created with IntelliJ IDEA.
 * User: zzl
 * Date: 13-1-25
 * To change this template use File | Settings | File Templates.
 */
(function ($, undefined) {

    var levelIndent = 24;

    var $table;
    var $tbody;
    var rootNode;
    var activeRow;
    var selectedRows = [];

    var rangeSelectBeginRow;
    var clickingRow;

    var afterDocClickTimer;
    var activeRowChanged;
    var checkNotifyActiveTodoChangeTimer;

    var _suppressMouseSelection;

    var todolist = {
    };
    todolist.setTable = function (table) {
        $table = $(table);
        $tbody = $table.children('tbody.nodes');

        initTree();

        setupEventHandlers();

        setActiveAndSelectRow(getFirstVisibleRow());

        //
        if (todolist.isEmpty()) {
            $table.children('tfoot.empty-indicator').show();
        }
    };
    todolist.getActiveRow = function () {
        return activeRow;
    };
    todolist.getSelectedRows = function () {
        return selectedRows.slice();
    };
    todolist.getRootNode = function () {
        return rootNode;
    };
    todolist.isEmpty = function () {
        return !domex.getNextElement(rootNode);
    };
    todolist.setActiveRow = function (row, keepSelection) {
        row = $(row)[0];
        if (keepSelection) {
            setActiveRow(row);
            focusActiveRow();
        }
        else {
            setActiveAndSelectRow(row);
        }
        checkNotifyActiveTodoChange();
    };
    todolist.suppressMouseSelection = function () {
        _suppressMouseSelection = true;
    };

    todolist.setNewIds = setNewIds;
    todolist.clearSelect = clearSelected;
    todolist.setupNodeRelations = setupNodeRelations;
    todolist.updateBranchLevel = updateBranchLevel;
    todolist.collectDescendants = collectDescendants;
    todolist.collectTreelineRelatedNodes = collectTreelineRelatedNodes;
    todolist.getLastChild = getLastChild;
    todolist.getParentNodes = getParentNodes;
    todolist.getParentNode = getParentNode;
    todolist.select = select;
    todolist.updateChildren = updateChildren;
    todolist.updateIndentLinesExtended = updateIndentLinesExtended;
    todolist.updateIndentLines = updateIndentLines;
    todolist.updateAltRows = updateAltRows;
    todolist.getSelectedSeparatedBranchNodes = getSelectedSeparatedBranchNodes;
    todolist.getNodeToActiveAfterDeletion = getNodeToActiveAfterDeletion;
    todolist.deleteNode = deleteNode;
    todolist.findNodeByPathTitles = findNodeByPathTitles;
    todolist.ensureNodeExpanded = ensureNodeExpanded;
    todolist.toggleRowsChecked = toggleRowsChecked;
    todolist.collapseAll = collapseAll;
    todolist.expandAll = expandAll;
    todolist.expandNode = expandNode;
    todolist.changeNodesRelation = changeNodesRelation;
    todolist.updateChildrenAndExpand = updateChildrenAndExpand;
    todolist.notifyRowVisibilityChanged = notifyRowVisibilityChanged;
    todolist.updateNodeVisibility = updateNodeVisibility;

    function initTree() {
        var $nodes = $tbody.children();
        rootNode = $nodes[0];

        setupNodeRelations($nodes);
    }

    function setupNodeRelations(nodes, index) {
        if (!nodes || nodes.length === 0) {
            return;
        }
        index = index || 0;
        var nodePath = [];
        var firstNode = nodes[0];
        var firstLevel = $(firstNode).data('level');
        nodePath[firstLevel] = firstNode;

        var parentNode;
        var prevLevel = firstLevel;
        var children;
        for (var n = index + 1; n < nodes.length; n++) {
            var node = nodes[n];
            var $node = $(node);
            var level = $(node).data('level');

            if (level <= firstLevel) {
                break;
            }
            nodePath[level] = node;
            if (level != prevLevel) {
                parentNode = nodePath[level - 1];
                var $parentNode = $(parentNode);
                children = $parentNode.data('children');
                if (!children) {
                    children = [];
                    $parentNode.data('children', children);
                }
            }
            children.push(node);
            $node.data('parent', parentNode);
            prevLevel = level;
        }
    }

    function updateBranchLevel(node, diff) {
        if (!diff) {
            return;
        }
        var $node = $(node);
        var children = $node.data('children') || [];
        for (var n = 0; n < children.length; n++) {
            updateBranchLevel(children[n], diff);
        }
        var level = diff + $node.data('level');

        $node.dataAttr('level', level);

        var $nodeContent = $node.find('.node-content');
        var $nodeButton = $($nodeContent.children()[0]);

        $nodeContent.css('padding-left', (levelIndent * level + 24) + 'px');
        $nodeButton.css('margin-left', level * levelIndent);
    }

    function collectTreelineRelatedNodes(nodes, resultNodes) {
        if (!resultNodes) {
            resultNodes = [];
        }
        if(nodes.length === undefined) {
            nodes = [nodes];
        }
        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            var $node = $(node);
            var level = $node.data('level');

            var nextNode = domex.getNextElement(node);
            while (nextNode) {
                if (jsex.inArray(nextNode, nodes)) {
                    break;
                }
                var nextLevel = $(nextNode).data('level');
                if (nextLevel > level) {
                    resultNodes.push(nextNode);
                }
                if (nextLevel <= level) {
                    break;
                }
                nextNode = domex.getNextElement(nextNode);
            }

            var parent = $node.data('parent');
            var parentChildren = $(parent).data('children');
            var isLastChild = parentChildren[parentChildren.length - 1] === node;
            if (!isLastChild) {
                continue;
            }

            var prevNode = domex.getPrevElement(node);
            while (1) {
                if (prevNode === rootNode || jsex.inArray(prevNode, nodes)) {
                    break;
                }
                var prevLevel = $(prevNode).data('level');
                if (prevLevel >= level) {
                    resultNodes.push(prevNode);
                }
                if (prevLevel <= level) {
                    break;
                }
                prevNode = domex.getPrevElement(prevNode);
            }

            //resultNodes.push(prevNode);

        }
        return resultNodes;
    }

    function collectDescendants(node, descendants) {
        if (!descendants) {
            descendants = [];
        }
        var children = $(node).data('children') || [];
        for (var n = 0; n < children.length; n++) {
            var child = children[n];
            descendants.push(child);
            collectDescendants(child, descendants);
        }
        return descendants;
    }

    function getLastChild(node) {
        var children = $(node).data('children');
        if (children && children.length) {
            return children[children.length - 1];
        }
        return null;
    }

    function getParentNodes(nodes, selfIfRoot) {
        var me = this;
        var parentNodes = [];
        for (var n = 0; n < nodes.length; n++) {
            var parentNode = me.getParentNode(nodes[n]);
            if (parentNode === me.rootNode && selfIfRoot) {
                parentNode = nodes[n];
            }
            if ($.inArray(parentNode, parentNodes) == -1) {
                parentNodes.push(parentNode);
            }
        }
        return parentNodes;
    }

    function getParentNode(node) {
        var $node = $(node);
        var result = $node.data('parent');
        return result || rootNode;
    }

    function updateChildren(node, directChildOnly) {
        if (!node || node.length === 0) {
            return;
        }
        var $node = $(node);
        var level = $node.data('level');
        if (!level) {
            $node.data('parent', rootNode);
        }
        var $next = $node.next();
        var children = [];
        while ($next.length) {
            var nextLevel = $next.data('level');
            if (nextLevel <= level) {
                break;
            }
            if (nextLevel - level == 1) {
                var child = $next[0];
                if (!directChildOnly) {
                    updateChildren(child);
                }
                children.push(child);
                $(child).data('parent', node);
            }
            $next = $next.next();
        }
        var oriChildren = $node.data('children');
        var childrenChanged = !oriChildren || oriChildren.length != children.length;
        $node.data('children', children);
        //
        if ($node[0] === rootNode) {
            //
        }
        else if (childrenChanged) { //xxxx
            if (children.length) {
                $node.removeClass('leaf').addClass('parent');
                if ($node.data('expanded')) {
                    $node.addClass('expanded').removeClass('collapsed');
                }
                else {
                    $node.addClass('collapsed').removeClass('expanded');
                }
            }
            else {
                $node.removeClass('parent').addClass('leaf');
            }
        }
    }

    function updateChildrenAndExpand(node, directChildOnly) {
        updateChildren(node, directChildOnly);
        if (isNodeCollapsed(node)) {
            expandNode(node);
        }
    }

    function updateIndentLinesExtended(nodes) {
        var relatedNodes = collectTreelineRelatedNodes(nodes);
        var allNodes = jsex.union(relatedNodes, nodes);
        updateIndentLines(allNodes);
    }

    function updateIndentLines(tr) {
        var n;
        if (!tr) {
            $tbody.children('tr').each(function () {
                updateIndentLines(this);
            });
            return;
        }
        else if (tr.length !== undefined) {
            for (n = 0; n < tr.length; n++) {
                updateIndentLines(tr[n]);
            }
            return;
        }
        if (tr === rootNode || !domex.inDomTree(tr)) {
            return;
        }
        var $tr = $(tr);
        var level = $tr.data('level');

        var td = $tr.children('td')[0];
        var indentWrapper = domex.getFirstChildElelement(td);
        var indentEl = domex.getFirstChildElelement(indentWrapper);
        var $indent = $(indentEl);

        var html;
        if (!level) {
            html = '<img src="image/line-udr.gif"/>';
            if (!getNextSiblingNode(tr)) {
                html = '<img src="image/line-ur.gif"/>';
            }
            $indent[0].innerHTML = html;
//            $indent.html(html);
            return;
        }
        var parent = $tr.data('parent');
        var parentChildren = $(parent).data('children');
        var isLastChild = parentChildren[parentChildren.length - 1] === tr;
        html = isLastChild ? '<img src="image/line-ur.gif"/>' : '<img src="image/line-udr.gif"/>';
        while (parent !== rootNode) {
            var parentParent = $(parent).data('parent');
            var parentIsLastChild;
            if (parentParent === rootNode) {
                parentIsLastChild = !getNextSiblingNode(parent);
            }
            else {
                var parentParentChildren = $(parentParent).data('children');
                parentIsLastChild = parentParentChildren[parentParentChildren.length - 1] === parent;
            }
            var parentHtml = parentIsLastChild ?
                '<img src="image/line-na.gif"/>' :
                '<img src="image/line-ud.gif"/>';
            html = parentHtml + html;
            parent = parentParent;
        }
        $indent[0].innerHTML = html;
//        $indent.html(html);
    }

    function getNextSiblingNode(node) {
        var $node = $(node);
        var parent = $node.data('parent');
        var children = $(parent).data('children');

        var index = jsex.indexOf(children, node);
        if (index == children.length - 1) {
            return null;
        }
        return children[index + 1];
    }

    function notifyRowVisibilityChanged() {
        $table.trigger('rowsVisibilityChanged');
        updateAltRows();
    }

    function updateAltRows() {
        if (!window.showAltRow) {
            return;
        }
        var $trs = $tbody.children();
        var visibleRowNo = 0;
        for (var n = 1; n < $trs.length; n++) {
            var $tr = $($trs[n]);
            if (!$tr.hasClass('hidden')) {
                ++visibleRowNo;
                $tr.toggleClass('alt', (visibleRowNo % 2) != 0);
            }
        }
    }

    function getSelectedSeparatedBranchNodes() {
        var nodes = selectedRows.slice();
        var allNodes = $tbody.children('tr');
        nodes.sort(function (node1, node2) {
            return $.inArray(node1, allNodes) - $.inArray(node2, allNodes);
        });
        filterOutBranchDescendantsForSortedNodes(nodes);
        return nodes;
    }

    function filterOutBranchDescendantsForSortedNodes(nodes) {
        var n = 0;
        while (n < nodes.length) {
            var descendants = collectDescendants(nodes[n++]);
            while ($.inArray(nodes[n], descendants) != -1 && n < nodes.length) {
                nodes.splice(n, 1);
            }
        }
    }

    function getPrevSiblingNode(node) {
        var $node = $(node);
        var parent = $node.data('parent');
        var children = $(parent).data('children');
        var index = jsex.indexOf(children, $node[0]);
        return index > 0 ? children[index - 1] : null;
    }

    function getNodeToActiveAfterDeletion(nodes) {
        var node0 = nodes[0];
        var resultNode;
        var node = node0;
        while (!resultNode && node) {
            node = getNextSiblingNode(node);
            if (jsex.indexOf(nodes, node, 1) == -1) {
                resultNode = node;
            }
        }
        node = node0;
        while (!resultNode && node) {
            node = getPrevSiblingNode(node);
            if (jsex.indexOf(nodes, node, 1) == -1) {
                resultNode = node;
            }
        }
        node = node0;
        while (!resultNode && node) {
            node = getParentNode(node);
            if (jsex.indexOf(nodes, node, 1) == -1) {
                resultNode = node;
            }
        }
        return resultNode;
    }

    function deleteNode(nodes) {
        var parentNodes = [];
        var affectedNodes = [];

        var treelineRelatedNodes = collectTreelineRelatedNodes(nodes);

        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            var parentNode = getParentNode(node);
            parentNodes.push(parentNode);
            var prev = domex.getPrevElement(node);
            if (prev !== rootNode && !jsex.inArray(prev, nodes)) {
                affectedNodes.push(prev);
            }

            deleteBranch(node);
        }
        parentNodes = jsex.distinct(parentNodes);
        for (var n = 0; n < parentNodes.length; n++) {
            updateChildren(parentNodes[n], true);
        }
        updateIndentLines(treelineRelatedNodes);

        $table.trigger('nodeDeleted');
    }

    function deleteBranch(node) {
        var $node = $(node);
        var children = $node.data('children') || [];
        for (var n = 0; n < children.length; n++) {
            deleteBranch(children[n]);
        }
        $node.remove();
    }

    //

    function setupEventHandlers() {
        $(document).on('mousemove', onDocumentMousemove);
        $(document).on('mouseup', onDocumentMouseup);
        $(document).on('click', onDocumentClick);
        $(document).on('keyup', onDocumentKeyup);
        $(document).on('selectstart', onDocumentSelectstart);
        $tbody.on('keydown', onTbodyKeydown);
        $tbody.on('mousedown', 'tr', onRowMousedown);
        $tbody.on('dblclick', 'tr', onRowDblclick);

        $tbody.on('boxSelected', onTbodyBoxSelected);
    }

    // handlers

    function onTbodyBoxSelected(e, rows) {
        select(rows);
        setActiveRow(clickingRow);
        clickingRow = null;
        clearTimeout(afterDocClickTimer);
    }

    function onDocumentSelectstart(e) {
        var nodeName = e.target.nodeName;
        if (nodeName == 'INPUT' || nodeName == 'TEXTAREA') {
            return;
        }
        e.preventDefault();
    }

    function onRowDblclick(e) {
        var node = this;
        if (!isParentNode(node)) {
            return;
        }
        domex.clearSelection();
        toggleNode(node);
        notifyRowVisibilityChanged();
    }

    function onRowMousedown(e) {
        clickingRow = this;
        _suppressMouseSelection = false;

        var $target = $(e.target);
        var $tab;
        if (!$target.is('[tabindex]')) {
            $tab = $target.parents('td[tabindex]:first');
            if (!$tab.length) {
                $tab = $(clickingRow).find('[tabindex]:first');
            }
        }
        else {
            $tab = $target;
        }
        setTimeout(function () {
            $tab[0].focus();
        }, 0);
    }

    function onDocumentKeyup(e) {
        checkNotifyActiveTodoChange();
    }

    function onDocumentClick(e) {
        var target = e.target;

        if (_suppressMouseSelection) {
            return;
        }
        if (!clickingRow) {
            return;
        }
        clearTimeout(afterDocClickTimer);
        var $target = $(target);
        if ($target.is('div.node-button')) {
            if (isParentNode(clickingRow)) {
                toggleNode(clickingRow);
                notifyRowVisibilityChanged();
                onRowClick(clickingRow, e);
            }
        }
        else if (e.target.nodeName == 'INPUT' && e.target.type == 'checkbox') {
            if (!isSelected(clickingRow)) {
                onRowClick(clickingRow, e);
            }
            else {
                setActiveRow(clickingRow);
            }
            //updateRowCheck();
            e.target.checked = !e.target.checked;
            toggleRowsChecked();
        }
        else {
            onRowClick(clickingRow, e);
        }
        clickingRow = null;
        checkNotifyActiveTodoChange();
    }

    function onDocumentMousemove(e) {
        //
    }

    function onDocumentMouseup(e) {
        if ($table.editable('isEditing') ||
            $table.dnd('isDragging')) {
            return;
        }
        if (_suppressMouseSelection) {
            return;
        }
        if(e.which != 1 && jsex.inArray(clickingRow, selectedRows)) {
            return;
        }
        afterDocClickTimer = setTimeout(function () {
            var scrolltop = $(window).scrollTop();
            if (!setActiveAndSelectRow(clickingRow)) {
                focusActiveRow();
            }
            $(window).scrollTop(scrolltop);
            clickingRow = null;
        }, 0);
    }

    function checkNotifyActiveTodoChange() {
//        console.log('************** checkNotifyActiveTodoChange ' + activeRowChanged + ', ' +
//            $(activeRow).find('span.label').text() + ' @' + new Date());
        clearTimeout(checkNotifyActiveTodoChangeTimer);
        checkNotifyActiveTodoChangeTimer = 0;
        if (activeRowChanged) {
            host.NotifyActiveTodoChanged();
            activeRowChanged = false;
        }
    }

    function onRowClick(row, e) {
        if (e.ctrlKey) {  // multi-select
            setActiveRow(row);
            setSelected(row, !isSelected(row));
        }
        else if (e.shiftKey) { // range-select
            var beginRow = rangeSelectBeginRow || activeRow;
            setActiveRow(row);
            rangeSelect($(beginRow).index(), $(row).index());
            rangeSelectBeginRow = beginRow;
        }
        else {
            setActiveAndSelectRow(row);
        }
    }

    function onTbodyKeydown(e) {
        var keyCode = e.which;
        var eat;
        var nodes;
        var action;

        var oriRangeSelectBeginRow = rangeSelectBeginRow || activeRow;

        if (e.ctrlKey) {
            if (keyCode == 38 || keyCode == 75) { //^up
                nodes = getSelectedSeparatedBranchNodes();
                action = undoman.onAction({
                    action: 'tree',
                    nodes: nodes
                });
                moveUp(nodes);
                action.afterAction();
                updateAltRows();
                focusActiveRow();
                host.NotifyChange();
                eat = true;
            }
            else if (keyCode == 40 || keyCode == 74) { // ^down
                nodes = getSelectedSeparatedBranchNodes();
                action = undoman.onAction({
                    action: 'tree',
                    nodes: nodes
                });
                moveDown(nodes);
                action.afterAction();
                updateAltRows();
                focusActiveRow();
                host.NotifyChange();
                eat = true;
            }
            else if (keyCode == 39 || keyCode == 76) { // ^right
                nodes = getSelectedSeparatedBranchNodes();
                action = undoman.onAction({
                    action: 'tree',
                    nodes: nodes
                });
                indent(nodes);
                action.afterAction();
                focusActiveRow();
                host.NotifyChange();
                eat = true;
            }
            else if (keyCode == 37 || keyCode == 72) { // ^left
                nodes = getSelectedSeparatedBranchNodes();
                action = undoman.onAction({
                    action: 'tree',
                    nodes: nodes
                });
                unindent(nodes);
                action.afterAction();
                updateAltRows();
                focusActiveRow();
                host.NotifyChange();
                eat = true;
            }
        }
        else if (keyCode == 38 || keyCode == 75) { // up/k
            if (!navigateToPrevRow()) {
                $(window).scrollTop($table.offset().top);
            }
            eat = true;
        }
        else if (keyCode == 40 || keyCode == 74) { // down/j
            if (!navigateToNextRow()) {
                //?
            }
            eat = true;
        }
        else if (keyCode == 37 || keyCode == 72) { // left/h
            if (isNodeExpanded(activeRow)) {
                collapseNode(activeRow);
                notifyRowVisibilityChanged();
            }
            else {
                var parentNode = getParentNode(activeRow);
                if (parentNode !== rootNode) {
                    setActiveAndSelectRow(parentNode);
                }
            }
            eat = true;
        }
        else if (keyCode == 39 || keyCode == 76) { // right/l
            if (isParentNode(activeRow)) {
                if (isNodeCollapsed(activeRow)) {
                    expandNode(activeRow);
                    notifyRowVisibilityChanged();
                }
                else {
                    navigateToNextRow();
                }
            }
            eat = true;
        }
        else if (keyCode == 34) { //pgdn
            eat = pageDown();
        }
        else if (keyCode == 33) { //pgup
            eat = pageUp();
        }
        else if (keyCode == 36) { //home
            eat = setActiveAndSelectRow(getFirstVisibleRow());
            if (eat) {
                $(window).scrollTop($table.offset().top);
            }
        }
        else if (keyCode == 35) { //end
            eat = setActiveAndSelectRow(getLastVisibleRow());
        }
        //
        else if (keyCode == 32 && !e.altKey && !e.ctrlKey && !e.shiftKey) {
            toggleRowsChecked();
            host.NotifyChange();
            eat = true;
        }

        if (e.shiftKey && oriRangeSelectBeginRow !== activeRow) {
            var fromIndex = $(oriRangeSelectBeginRow).index();
            rangeSelect(fromIndex, $(activeRow).index());
            rangeSelectBeginRow = oriRangeSelectBeginRow;
        }

        if (eat) {
            e.preventDefault();
        }
    }

    function unindent(node) {
        if (node.length) {
            for (var n = node.length - 1; n >= 0; --n) {
                unindent(node[n]);
            }
            return;
        }
        var parent = getParentNode(node);
        if (parent === rootNode) {
            return;
        }
        var parentChildren = $(parent).data('children');
        parentChildren.splice(jsex.indexOf(parentChildren, node), 1);
        if (!parentChildren.length) {
            $(parent).removeClass('parent expanded').addClass('leaf');
        }

        //*
        var tbody = node.parentNode;
        var descendants = [];
        collectDescendants(node, descendants);
        tbody.removeChild(node);
        var n;
        for (n = 0; n < descendants.length; n++) {
            tbody.removeChild(descendants[n]);
        }
        var beforeNode = null;

        var tParent = parent;
        while (tParent !== rootNode && beforeNode == null) {
            beforeNode = getNextSiblingNode(tParent);
            tParent = getParentNode(tParent);
        }

        tbody.insertBefore(node, beforeNode);
        for (n = 0; n < descendants.length; n++) {
            tbody.insertBefore(descendants[n], beforeNode);
        }
        //
        var grandParent = getParentNode(parent);
        $(node).data('parent', grandParent);
        var grandParentChildren = $(grandParent).data('children');
        grandParentChildren.splice(jsex.indexOf(grandParentChildren, parent) + 1, 0, node);

        //*/
        updateBranchLevel(node, -1);

        updateIndentLinesExtended([parent, node]);
    }

    function indent(node) {
        if (node.length) {
            for (var n = 0; n < node.length; n++) {
                indent(node[n]);
            }
            return;
        }

        var prev = getPrevSiblingNode(node);
        if (!prev) {
            return;
        }
        var parent = getParentNode(node);
        var parentChildren = $(parent).data('children');
        parentChildren.splice(jsex.indexOf(parentChildren, node), 1);

        var $prev = $(prev);
        var children;
        if ($prev.is('.leaf')) {
            $prev.removeClass('leaf').addClass('parent expanded').dataAttr('expanded', 1);
            children = [];
            $prev.data('children', children);
        }
        else {
            children = $prev.data('children');
            expandNode($prev);
        }
        children.push(node);
        $(node).data('parent', $prev[0]);
        updateBranchLevel(node, 1);
        updateIndentLinesExtended([$prev[0], node]);
    }

    function moveUp(node) {
        var n;
        if (node.length) {
            for (n = 0; n < node.length; n++) {
                moveUp(node[n]);
            }
            return;
        }
        var prev = getPrevSiblingNode(node);
        if (!prev) {
            return;
        }
        var parent = getParentNode(node);
        var parentChildren = parent && $(parent).data('children');
        var index = jsex.indexOf(parentChildren, node);

        var tbody = node.parentNode;
        var descendants = [];
        collectDescendants(node, descendants);
        node = tbody.removeChild(node);

        for (n = 0; n < descendants.length; n++) {
            tbody.removeChild(descendants[n]);
        }

        node = tbody.insertBefore(node, prev);

        for (n = 0; n < descendants.length; n++) {
            tbody.insertBefore(descendants[n], prev);
        }

        parentChildren.splice(index, 1);
        parentChildren.splice(index - 1, 0, node);
        updateIndentLinesExtended([prev, node]);
    }

    function moveDown(node) {
        var n;
        if (node.length) {
            for (n = node.length - 1; n >= 0; --n) {
                moveDown(node[n]);
            }
            return;
        }
        var next = getNextSiblingNode(node);
        if (!next) {
            return;
        }

        var tbody = node.parentNode;
        var descendants = [];
        collectDescendants(next, descendants);
        next = tbody.removeChild(next);

        for (n = 0; n < descendants.length; n++) {
            tbody.removeChild(descendants[n]);
        }

        next = tbody.insertBefore(next, node);

        for (n = 0; n < descendants.length; n++) {
            tbody.insertBefore(descendants[n], node);
        }

        //
        var parent = $(node).data('parent');
        var children = $(parent).data('children');
        var index = jsex.indexOf(children, node);
        children[index] = next;
        children[index + 1] = node;
        updateIndentLinesExtended([node, next]);
    }

    // helpers

    function pageUp() {
        var $activeRow = $(activeRow);
        var y = $activeRow.offset().top;
        var h = $activeRow.outerHeight();
        var viewport = domex.getViewport();
        var $prevTrs = $activeRow.prevAll(':visible');
        if (!$prevTrs.length) {
            if (y < viewport.y1 || y + h > viewport.y2) {
                $(window).scrollTop(y);
                return true;
            }
            return false;
        }
        var $prevTr = $($prevTrs[0]);

        var y2 = viewport.y2;
        if (y < viewport.y1 || y > viewport.y2 ||
            $prevTr.offset().top < viewport.y1) {
            y2 = y + h;
        }
        var y1 = y2 - (viewport.y2 - viewport.y1);
        var $toSelectTr;
        for (var n = 0; n < $prevTrs.length; n++) {
            $prevTr = $($prevTrs[n]);
            if ($prevTr.offset().top < y1) {
                break;
            }
            $toSelectTr = $prevTr;
        }
        if (!$toSelectTr) {
            return false;
        }
        y1 = $toSelectTr.offset().top;
        if (y1 < viewport.y1) {
            $(window).scrollTop(y1);
        }
        setActiveAndSelectRow($toSelectTr[0]);
        return true;
    }

    function pageDown() {
        var $activeRow = $(activeRow);
        var y = $activeRow.offset().top;
        var h = $activeRow.outerHeight();
        var viewport = domex.getViewport();
        var $nextTrs = $activeRow.nextAll(':visible');
        if (!$nextTrs.length) {
            if (y + h > viewport.y2 || y < viewport.y1) {
                $(window).scrollTop(y + h - (viewport.y2 - viewport.y1));
                return true;
            }
            return false;
        }
        var $nextTr = $($nextTrs[0]);

        var y1 = viewport.y1;
        if (y < viewport.y1 || y > viewport.y2 ||
            $nextTr.offset().top + $nextTr.outerHeight() > viewport.y2) {
            y1 = y;
        }
        var y2 = y1 + (viewport.y2 - viewport.y1);
        var $toSelectTr;
        for (var n = 0; n < $nextTrs.length; n++) {
            $nextTr = $($nextTrs[n]);
            if ($nextTr.offset().top + $nextTr.outerHeight() > y2) {
                break;
            }
            $toSelectTr = $nextTr;
        }
        if (!$toSelectTr) {
            return false;
        }
        y2 = $toSelectTr.offset().top + $toSelectTr.outerHeight();
        if (y2 > viewport.y2) {
            $(window).scrollTop(y2 - (viewport.y2 - viewport.y1));
        }
        setActiveAndSelectRow($toSelectTr[0]);
        return true;
    }

    function getFirstVisibleRow() {
        return getNextVisibleRow(rootNode);
    }

    function getLastVisibleRow() {
        var lastRow = $tbody.children(':last-child')[0];
        if (!domex.isVisible(lastRow)) {
            lastRow = getPrevVisibleRow(lastRow)
        }
        return lastRow;
    }

    function getParentNode(node) {
        return $(node).data('parent');
    }

    function isLeafRow(row) {
        return $(row).hasClass('leaf');
    }

    function isParentNode(row) {
        return $(row).hasClass('parent');
    }

    function isNodeCollapsed(row) {
        var $row = $(row);
        return $row.hasClass('parent') && !$row.data('expanded');
    }

    function isNodeExpanded(row) {
        var $row = $(row);
        return $row.hasClass('parent') && $row.data('expanded');
    }

    function navigateToPrevRow() {
        var prevRow = getPrevVisibleRow(activeRow);
        if (!prevRow) {
            return false;
        }
        setActiveAndSelectRow(prevRow);
        return true;
    }

    function navigateToNextRow() {
        var prevRow = getNextVisibleRow(activeRow);
        if (!prevRow) {
            return false;
        }
        setActiveAndSelectRow(prevRow);
        return true;
    }

    function getPrevVisibleRow(row) {
        while (row = domex.getPrevElement(row)) {
            if (domex.isVisible(row)) {
                return row;
            }
        }
        return null;
    }

    function getNextVisibleRow(row) {
        while (row = domex.getNextElement(row)) {
            if (domex.isVisible(row)) {
                return row;
            }
        }
        return null;
    }

    function showBranch($node) {
        $node.removeClass('hidden');

        if ($node.data('expanded')) {
            var children = $node.data('children') || [];
            for (var n = 0; n < children.length; n++) {
                showBranch($(children[n]));
            }
        }
    }

    function hideDescendants($node) {
        var children = $node.data('children') || [];
        for (var n = 0; n < children.length; n++) {
            var $child = $(children[n]);
            hideDescendants($child);
            $child.addClass('hidden');
        }
    }


    // api

    function toggleNode(node) {

        if (isNodeExpanded(node)) {
            collapseNode(node);
        }
        else {
            expandNode(node);
        }
    }

    function collapseNode(node) {
        var $node = $(node);
        $node.removeClass('expanded').addClass('collapsed');
        $node.dataAttr('expanded', 0);
        hideDescendants($node);
    }

    function expandNode(node) {
        var $node = $(node);
        $node.removeClass('collapsed').addClass('expanded');
        $node.dataAttr('expanded', 1);
        showBranch($node);
    }

    function setActiveAndSelectRow(row) {
        if (!domex.inDomTree(row)) {
            return false;
        }
        setActiveRow(row);
        focusActiveRow();

        clearSelected();
        setSelected(row);
        return true;
    }

    function setActiveRow(row) {
//        console.log('************ setActiveRow');
        clearTimeout(checkNotifyActiveTodoChangeTimer);
        checkNotifyActiveTodoChangeTimer = setTimeout(checkNotifyActiveTodoChange, 300);
        rangeSelectBeginRow = null;
        if (activeRow === row) {
            return;
        }
        $(activeRow).removeClass('active');
        activeRow = row;
        $(activeRow).addClass('active');
        activeRowChanged = true;
    }

    function focusActiveRow() {
        if (!domex.inDomTree(activeRow)) {
            return;
        }
        var el = document.activeElement;
        if (!$.contains(activeRow, el)) {
            $(activeRow).find('span.label')[0].focus();
        }
    }

    function setSelected(row, selected) {
        if (selected === undefined || selected) {
            selectedRows.push(row);
            $(row).addClass('selected');
        }
        else {
            var index = jsex.indexOf(selectedRows, row);
            selectedRows.splice(index, 1);
            $(row).removeClass('selected');
        }
    }

    function select(rows) {
        console.log('select ' + rows.length);
        $(selectedRows).removeClass('selected');
        selectedRows = [];
        for (var n = 0; n < rows.length; n++) {
            var row = rows[n];
            if (!$(row).hasClass('hidden')) {
                selectedRows.push(row);
            }
        }
        $(selectedRows).addClass('selected');
    }

    function rangeSelect(fromIndex, toIndex) {
        clearSelected();
        if (fromIndex > toIndex) {
            var t = toIndex;
            toIndex = fromIndex;
            fromIndex = t;
        }
        var $trs = $tbody.children('tr');
        for (var index = fromIndex; index <= toIndex; index++) {
            var tr = $trs[index];
            var $tr = $(tr);
            if ($tr.hasClass('hidden')) {
                continue;
            }
            selectedRows.push(tr);
            $tr.addClass('selected');
        }
    }

    function isSelected(row) {
        return jsex.inArray(row, selectedRows);
    }

    function clearSelected() {
//        console.log('clearselected');
        $(selectedRows).removeClass('selected');
        selectedRows = [];
    }

    function _findNodeByPathTitles(titles, level, nodes) {
        if (!nodes) {
            return null;
        }
        var title = titles[level];
        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            var nodeTitle = $(node).find('.node-content').text();
            nodeTitle = jsex.trim(nodeTitle);
            if (nodeTitle == title) {
                if (level == titles.length - 1) {
                    return node;
                }
                var tNode = _findNodeByPathTitles(titles, level + 1, $(node).data('children'));
                if (tNode) {
                    return tNode;
                }
            }
        }
        return null;
    }

    function findNodeByPathTitles(pathTitles) {
        return _findNodeByPathTitles(pathTitles, 0, $(rootNode).data('children'));
    }

    function ensureNodeExpanded(node) {
        var parent = $(node).data('parent');
        while (parent && parent !== rootNode) {
            expandNode(parent, true);
            parent = $(parent).data('parent');
        }
    }

    function toggleRowsChecked() {
        var $chk = $(activeRow).find(':checkbox');
        var newCheck = !$chk.is(':checked');

        var nodes = selectedRows.slice();
        var oriChecks = [];
        var deactivatedRows = [];

        for (var n = 0; n < nodes.length; n++) {
            var $row = $(nodes[n]);
            var $chk = $row.find(':checkbox');

            var oriChecked = $chk.is(':checked');
            oriChecks.push(oriChecked);

            $chk[0].checked = newCheck;
            $row.toggleClass('checked', newCheck);

            if(newCheck && !oriChecked) {
                //what if undo/redoed?
                $row.attr('data-checkTime', jsex.formatDate(new Date(), 'yyMMddhhmm'));
            }

            //
            if (newCheck && $row.hasClass('active-todo')) {
                $row.removeClass('active-todo');
                deactivatedRows.push($row[0]);
            }
        }

        var deactivated = deactivatedRows.length != 0;
        if (deactivated) {
            undoman.beginCompoundAction();
            undoman.onAction({
                action: 'cssClass',
                nodes: deactivatedRows,
                cssClass: 'active-todo',
                oriState: true,
                newState: false
            });
        }


        undoman.onAction({
            action: 'check',
            nodes: nodes,
            oriChecked: oriChecks,
            newChecked: newCheck
        });


        deactivated && undoman.endCompoundAction();
        host.NotifyChange();
    }

    function collapseAll() {
        var $nodes = $tbody.children();
        for (var n = 1; n < $nodes.length; n++) {
            var $node = $($nodes[n]);
            if ($node.is('.parent') && $node.data('expanded')) {
                collapseNode($node);
            }
        }
        notifyRowVisibilityChanged();
        ensureVisibleActiveRow();
    }

    function ensureVisibleActiveRow() {
        var newActiveRow;
        if ($(activeRow).hasClass('hidden')) {
            var row = getParentNode(activeRow);
            while (row !== rootNode) {
                if (!$(row).hasClass('hidden')) {
                    newActiveRow = row;
                    break;
                }
                row = getParentNode(row);
            }
        }
        if(newActiveRow) {
            setActiveAndSelectRow(newActiveRow);
        }
    }

    function expandAll() {
        var $nodes = $tbody.children();
        for (var n = 1; n < $nodes.length; n++) {
            var $node = $($nodes[n]);
            if ($node.is('.parent') && !$node.data('expanded')) {
                expandNode($node);
            }
        }
        notifyRowVisibilityChanged();
    }

    function changeNodesRelation(nodes, toNode, below, asChild) {

        var $toNode = $(toNode);
        var nodesToChange = [];
        var nodeParents = [];

//        todolist.collectTreelineRelatedNodes(nodes, treeLineRelatedNodes)

        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            nodeParents.push(getParentNode(node));

            var $node = $(node);
            var levelDiff = $toNode.data('level') - $node.data('level');
            if (asChild) {
                levelDiff += 1;
            }
            if (levelDiff) {
                updateBranchLevel(node, levelDiff);
            }
            nodesToChange.push(node);
            collectDescendants(node, nodesToChange);
        }
        nodeParents = jsex.distinct(nodeParents);
        $(nodesToChange).remove();

        if (!asChild && !below) { //above
            $toNode.before(nodesToChange);
        }
        else {
            if (jsex.inArray(toNode, nodeParents)) { //if toNode children removed, update it
                updateChildren(toNode);
            }
            var toNodeDescendants = [toNode];
            collectDescendants(toNode, toNodeDescendants);
            if (asChild && !below) { // child above
                $(toNode).after(nodesToChange);
            }
            else { //below || chile below
                $(toNodeDescendants[toNodeDescendants.length - 1]).after(nodesToChange);
            }
        }
        nodeParents.push(asChild ? toNode : getParentNode(toNode));
        nodeParents = jsex.distinct(nodeParents);

        for (n = 0; n < nodeParents.length; n++) {
            updateChildren(nodeParents[n]);
        }
        if (asChild) {
            expandNode(toNode);
        }

        var treeLineRelatedNodes = [];
        todolist.collectTreelineRelatedNodes(nodes, treeLineRelatedNodes)
        treeLineRelatedNodes = jsex.union(treeLineRelatedNodes, nodes);
        todolist.updateIndentLines(treeLineRelatedNodes);

        return nodesToChange;
    }

    //
    function setNewIds(newIdMap) {
        var $rows = $tbody.children();
        for(var index in newIdMap) {
            var id = newIdMap[index];
            var $row = $($rows[Number(index) + 1]);
            $row.dataAttr('id', id);
        }
    }

    function updateNodeVisibility(node) {
        if(node.length) {
            for(var n = 0; n < node.length; n++) {
                updateNodeVisibility(node[n]);
            }
            return;
        }
        var $node = $(node);
        if($node.hasClass('parent')) {
            if ($node.data('expanded')) {
                showBranch($node);
            }
            else {
                hideDescendants($node);
            }
        }
    }

    window.todolist = todolist;

})(jQuery);